完成會員註冊後,今天要實作「會員登入」功能。
當使用者輸入帳號與密碼時,後端需要做兩件事:
password_hash() 亂碼是否吻合。password_verify()在上一篇文章有提到,密碼經過雜湊後是不可逆的,既然無法解密,該怎麼驗證使用者輸入的密碼是否正確?
PHP 提供了一個非常方便且安全的函式:password_verify()。
// 語法:password_verify(使用者輸入的明文密碼, 資料庫裡的雜湊碼)
\(isPasswordCorrect = password_verify(\)inputPassword, $dbPasswordHash);
password_verify() 會自動分析資料庫那串雜湊碼裡的「鹽值」與演算法參數,拿使用者這次輸入的密碼做相同的數學計算。如果算出來結果一致,就會回傳 true,否則回傳 false。
在專案目錄下啟動 Claude Code 下達需求:
請幫我寫一個 login.php。前端要包含帳號(Email)與密碼表單;後端接收 POST 請求後,查詢 members 資料表是否存在該帳號。如果有找到,使用 password_verify() 驗證密碼。密碼正確時,啟動 Session 並將會員 ID 與姓名存入 $_SESSION,最後導向首頁 index.php;若驗證失敗,顯示適當的錯誤訊息。
生成的 login.php:
<?php
// ============================================================
// 區塊:啟動 Session
// ============================================================
session_start(); // 必須在任何 HTML 輸出之前呼叫,之後才能讀寫 $_SESSION
// ============================================================
// 區塊:載入資料庫連線
// ============================================================
require __DIR__ . '/config/database.php'; // 載入 Database 類別定義
$pdo = Database::getConnection(); // 取得 PDO 連線實例
// ============================================================
// 區塊:初始化表單狀態
// ============================================================
$errors = []; // 存放錯誤訊息的陣列
$email = ''; // 帳號(email)欄位,登入失敗重新顯示表單時用來保留使用者輸入
// ============================================================
// 區塊:處理表單送出(POST)
// ============================================================
if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 只有 POST 才進行登入驗證,GET 只顯示空白表單
$email = trim($_POST['email'] ?? ''); // 取得帳號(email)並去除頭尾空白
$password = (string) ($_POST['password'] ?? ''); // 取得密碼,密碼不 trim 以免誤刪使用者刻意輸入的空白
// ------------------------------------------------------------
// 子區塊:檢查必填欄位是否為空
// ------------------------------------------------------------
if ($email === '') {
$errors[] = '請輸入帳號 (email)'; // 帳號為空
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = '帳號格式不正確,請輸入有效的 email'; // 帳號格式錯誤
}
if ($password === '') {
$errors[] = '請輸入密碼'; // 密碼為空
}
// ------------------------------------------------------------
// 子區塊:查詢 members 資料表並驗證密碼
// ------------------------------------------------------------
if (empty($errors)) { // 前面欄位驗證都通過才查資料庫,避免無意義的查詢
$sql = 'SELECT id, name, password FROM members WHERE email = :email'; // 依 email 查出會員 ID、姓名與密碼雜湊
$stmt = $pdo->prepare($sql); // 準備查詢語句,使用 prepared statement 防止 SQL injection
$stmt->execute([':email' => $email]); // 綁定帳號並執行查詢
$member = $stmt->fetch(); // 查無資料時回傳 false
// 找到帳號且 password_verify() 比對雜湊成功,才算登入成功
if ($member !== false && password_verify($password, $member['password'])) {
session_regenerate_id(true); // 登入成功後更換 Session ID,防止 session fixation 攻擊
$_SESSION['member_id'] = $member['id']; // 存入會員 ID,供後續頁面判斷登入者
$_SESSION['member_name'] = $member['name']; // 存入會員姓名,供頁面顯示
header('Location: index.php'); // 登入成功導向首頁
exit; // 導向後立刻結束,避免下方 HTML 繼續輸出
}
// 帳號不存在與密碼錯誤顯示同一則訊息,避免讓人藉此猜出哪些 email 已註冊
$errors[] = '帳號或密碼錯誤,請重新輸入';
}
}
?>
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>會員登入 - 圖書管理系統</title>
<style>
body {
font-family: "Microsoft JhengHei", Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
margin: 0;
padding: 2rem;
}
h1 {
text-align: center;
margin-bottom: 1.5rem;
}
form {
width: 100%;
max-width: 480px;
margin: 0 auto;
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 1.5rem 2rem;
box-sizing: border-box;
}
.field {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.35rem;
font-weight: bold;
}
input[type="email"],
input[type="password"] {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
font-size: 1rem;
}
button {
width: 100%;
padding: 0.75rem;
background-color: #2c3e50;
color: #fff;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
button:hover {
background-color: #1a252f;
}
.errors {
max-width: 480px;
margin: 0 auto 1rem;
background-color: #fdecea;
color: #b71c1c;
border: 1px solid #f5c6cb;
border-radius: 4px;
padding: 0.75rem 1rem;
}
.errors ul {
margin: 0;
padding-left: 1.25rem;
}
.back-link {
display: block;
max-width: 480px;
margin: 1rem auto 0;
text-align: center;
}
</style>
</head>
<body>
<h1>會員登入</h1>
<?php // 區塊:顯示欄位驗證或登入失敗的錯誤訊息 ?>
<?php if (!empty($errors)): ?>
<div class="errors">
<ul>
<?php foreach ($errors as $error): ?>
<li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<!-- 區塊:登入表單,欄位包含帳號(email)與密碼 -->
<form method="POST" action="login.php">
<div class="field">
<label for="email">帳號 (email)</label>
<input type="email" id="email" name="email" value="<?= htmlspecialchars($email) ?>" required>
</div>
<div class="field">
<label for="password">密碼</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">登入</button>
</form>
<a class="back-link" href="register.php">還沒有帳號?前往註冊</a>
</body>
</html>
session_start() 的位置:$_SESSION 的 PHP 檔案最上方,都必須呼叫 session_start()。而且它前面不能有任何 HTML 輸出或空白行,否則會觸發 Headers already sent 警告。' OR '1'='1 進行 SQL Injection 繞過驗證。login.php,輸入註冊好的帳號(user1@example.com)與密碼。按下登入後,成功跳轉至 index.php。今天完成了會員登入功能,並利用 password_verify() 與 Session 建立起使用者的登入狀態。
明天將把 Session 應用在整個系統中,實作「登出功能」與「未登入使用者的權限控管」,確保只有登入的會員才能看到或操作特定功能。